﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.CSharp;

namespace Utilities.DynamicValues
{
    public sealed class DynamicValueCompiler<TValue> : IDynamicValueCompiler<TValue>
    {
        private readonly IDictionary<string, TValue> m_SourceCodeToValue = new Dictionary<string, TValue>();

        public TValue GetOrCompile(string valueSourceCode, params Type[] referencedTypes)
        {
            TValue value;
            if (!m_SourceCodeToValue.TryGetValue(valueSourceCode, out value))
            {
                value = Compile(valueSourceCode, referencedTypes);
                m_SourceCodeToValue.Add(valueSourceCode, value);
            }
            return value;
        }

        private static TValue Compile(string valueSourceCode, params Type[] referencedTypes)
        {
            // Add implicit type references and find unique set of referenced types.
            IDictionary<Type, bool> distinctReferencedTypes = new Dictionary<Type, bool>();
            AddTypes(typeof(void), distinctReferencedTypes);
            AddTypes(typeof(TValue), distinctReferencedTypes);
            AddTypes(referencedTypes, distinctReferencedTypes);

            // Find unique set of referenced assemblies.
            IDictionary<Assembly, bool> distinctReferencedAssemblies = new Dictionary<Assembly, bool>();
            foreach (Type referencedType in distinctReferencedTypes.Keys)
            {
                Assembly referencedAssembly = referencedType.Assembly;
                distinctReferencedAssemblies[referencedAssembly] = true;
            }

            return Compile(valueSourceCode, distinctReferencedTypes.Keys, distinctReferencedAssemblies.Keys);
        }

        private static void AddTypes(IEnumerable<Type> referencedTypes, IDictionary<Type, bool> distinctReferencedTypes)
        {
            foreach (Type referencedType in referencedTypes)
            {
                AddTypes(referencedType, distinctReferencedTypes);
            }
        }

        private static void AddTypes(Type referencedType, IDictionary<Type, bool> distinctReferencedTypes)
        {
            distinctReferencedTypes[referencedType] = true;
            AddTypes(referencedType.GetGenericArguments(), distinctReferencedTypes);
        }

        private static TValue Compile(string valueSourceCode, IEnumerable<Type> distinctReferencedTypes, IEnumerable<Assembly> distinctReferencedAssemblies)
        {
            IDictionary<string,string> providerOptions = new Dictionary<string,string>();
            providerOptions.Add("CompilerVersion", "v4.0");

            CSharpCodeProvider codeProvider = new CSharpCodeProvider(providerOptions);

            CompilerParameters compilerParameters = new CompilerParameters();
            compilerParameters.GenerateExecutable = false;
            compilerParameters.GenerateInMemory = true;
            compilerParameters.TreatWarningsAsErrors = true;

            foreach (Assembly referencedAssembly in distinctReferencedAssemblies)
            {
                compilerParameters.ReferencedAssemblies.Add(referencedAssembly.Location);
            }

            string namespaceName = typeof(DynamicValueCompiler<>).Namespace;
            string dynamicValueTypeName = "DynamicValue" + Guid.NewGuid().ToString().Replace("-", "");
            string dynamicValueTypeFullName = namespaceName + "." + dynamicValueTypeName;
            const string valueFieldName = "Value";

            // Compile unit
            CodeCompileUnit codeCompileUnit = new CodeCompileUnit();
            {
                // Namespace
                CodeNamespace codeNamespace = new CodeNamespace(namespaceName);
                {
                    // Usings
                    foreach (Type referencedType in distinctReferencedTypes)
                    {
                        codeNamespace.Imports.Add(new CodeNamespaceImport(referencedType.Namespace));
                    }

                    // Type
                    CodeTypeDeclaration codeTypeDeclaration = new CodeTypeDeclaration(dynamicValueTypeName);
                    codeTypeDeclaration.TypeAttributes = TypeAttributes.Public;
                    {
                        // Field
                        CodeMemberField codeMemberField = new CodeMemberField(typeof(TValue), valueFieldName);
                        codeMemberField.Attributes = MemberAttributes.Public;
                        codeMemberField.InitExpression = new CodeSnippetExpression(valueSourceCode);
                        codeTypeDeclaration.Members.Add(codeMemberField);
                    }
                    codeNamespace.Types.Add(codeTypeDeclaration);
                }
                codeCompileUnit.Namespaces.Add(codeNamespace);
            }

            // Uncomment to print source code to console for debugging.
            //using (StringWriter writer = new StringWriter())
            //{
            //    codeProvider.GenerateCodeFromCompileUnit(compileUnit, writer, new CodeGeneratorOptions());
            //    string compileUnitSourceCode = writer.ToString();
            //    Console.WriteLine(compileUnitSourceCode);
            //}

            CompilerResults compilerResults = codeProvider.CompileAssemblyFromDom(compilerParameters, codeCompileUnit);

            if (compilerResults.Errors.HasErrors)
            {
                IEnumerable<CompilerError> compilerErrors = compilerResults.Errors.Cast<CompilerError>().ToList();
                throw new DynamicCompilationException(compilerErrors);
            }

            Type dynamicValueType = compilerResults.CompiledAssembly.GetType(dynamicValueTypeFullName);
            object dynamicValueInstance = Activator.CreateInstance(dynamicValueType);
            FieldInfo valueFieldInfo = dynamicValueType.GetField(valueFieldName, BindingFlags.Public | BindingFlags.Instance);
            TValue value = (TValue)valueFieldInfo.GetValue(dynamicValueInstance);
            return value;
        }
    }
}